module ChessData

  # Represents a chess game, as read in from a PGN file.
  #
  # Header information in a pgn is stored in a hash table, and method_missing 
  # used to provide an accessor-like mechanism for storing/retrieving header 
  # information. For example:
  #
  #   db = Database.new
  #   db.add_games_from "test/data/fischer.pgn"
  #   game = db[0]
  #   puts game.event
  #   puts game.white
  #   puts game.black
  #
  # Each of the 'event', 'white', 'black' values are retrieved from the 
  # PGN header. 
  #
  # New key values can be created and assigned to, to construct a header for 
  # a game. For example:
  #
  #   db = Database.new
  #   game = Game.new
  #   game.white = "Peter"
  #   game.black = "Paul"
  #   game.result = "1-0"
  #   game << "e4"
  #   db << game
  #   db.to_file "mygames.pgn"
  #
  # And the pgn file will contain:
  #
  #   [Result "1-0"]
  #   [White "Peter"]
  #   [Black "Paul"]
  #   
  #   1. e4  1-0
  # 
  #
  class Game
    # Stores the sequence of moves in the game.
    attr_accessor :moves

    def initialize
      @header = {}
      @moves = []
    end

    # method_missing is used for accessing key-value terms in the header.
    # * Any unknown method call is checked if it is the key of a header 
    #   item and, if so, the value for that key is returned. 
    # * If the unknown method has an '=' sign in it, a new key is 
    #   created and assigned a value, which must be an argument to method.
    def method_missing iname, *args
      name = iname.to_s 
      if @header.has_key? name
        @header[name]
      elsif name.include? "=" # assign, so create new key
        key = name.delete "="
        @header[key] = args[0]
      else
        puts "Unknown key '#{name}' for header #{@header}"
        super 
      end
    end

    # Append given _move_ to list of moves.
    # Given move can be a valid move type or a string.
    #
    # An InvalidMoveError is raised if the move is not valid.
    #
    def << move
      if move.respond_to? :make_move
        @moves << move
      elsif
        move.kind_of? String
        @moves << Moves.new_move(move)
      else
        raise InvalidMoveError.new("Invalid type of move: #{move}")
      end
    end

    # Return the number of half-moves in the game.
    def half_moves 
      @moves.size
    end

    # Return the start position for the game.
    # PGN games may provide a start-position, if they do not begin from the start position.
    def start_position
      if @header.has_key? "fen"
        ChessData::Board.from_fen @header["fen"]
      else
        ChessData::Board.start_position
      end
    end

    # Step through the game from start position, one half-move at a time.
    # Yields to a block the current board position and the next move.
    # Yields final board position and result at end of game.
    def play_game
      board = start_position
      @moves.each do |move|
        yield board, move
        board = move.make_move board
      end
      yield board, result
    end

    # Test if game meets the position definition given in the block
    # using Game#play_game to step through the game.
    def search &block
      defn = ChessData::PositionDefinition.new(&block)
      play_game do |board|
        return true if defn.check board
      end
      return false
    end

    # Write game in PGN format to given IO stream.
    # This method is usually called from Database#to_file
    # but can also be called directly.
    #
    def to_pgn stream
      @header.keys.each do |key|
        stream.puts "[#{key.capitalize} \"#{@header[key]}\"]"
      end
      stream.puts # blank separating line
      move_str = ""
      move_number = 1
      @moves.each_slice(2) do |full_move|
        move_str += "#{move_number}. #{full_move[0]} #{full_move[1]} "
        move_number += 1
      end
      move_str += result
      stream.puts WordWrap.ww(move_str, 80)
    end

    # Regular expression used to match a PGN header line.
    MatchHeader = /\[(\w+) \"(.*)\"\]/

    # Reads a single game from a given IO stream.
    # Returns nil if failed to read a game or its moves.
    def Game.from_pgn stream
      game = Game.new
      moves = []
      # ignore blank lines
      begin
        line = stream.gets
        return nil if line.nil? # failed to read game/empty file
      end while line.strip.empty?
      # read the header
      while MatchHeader =~ line
        game.send "#{$1.downcase}=", $2
        line = stream.gets.strip
      end
      # ignore blank lines
      begin
        line = stream.gets
        return nil if line.nil? # failed to read moves for game
      end while line.strip.empty?
      # read the moves
      begin
        next if line.start_with? "%"
        semi_index = line.index ";" # look for ; comment start
        line = line[0...semi_index] if semi_index # and strip it
        moves << line.strip 
        line = stream.gets
      end until line.nil? || line.strip.empty? || MatchHeader =~ line
      # return an event line if it immediately follows the moves
      # so can be read for next game
      stream.ungetc line if MatchHeader =~ line
      # parse the moves and add to game
      move_str = moves.join(" ")
      if /{.*}/.match(move_str) # remove { } comments
        move_str = $` + " " + $'
      end
      move_str.split(" ").each do |token|
        case token
        when "1-0", "0-1", "1/2-1/2", "*" then game.result = token
        when /^\d+/ then ; # ignore the move number
        when /^$\d+/ then ; # ignore NAG
        when Moves::LegalMove then game << Moves.new_move(token)
        end
      end

      return game
    end
  end
end

